package org.checkerframework.framework.type;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.checkerframework.checker.signature.qual.CanonicalName;
import org.checkerframework.framework.qual.AnnotatedFor;
import org.checkerframework.framework.util.DefaultQualifierKindHierarchy;
import org.checkerframework.framework.util.QualifierKind;
import org.checkerframework.framework.util.QualifierKindHierarchy;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TypeSystemError;

/**
 * A {@link QualifierHierarchy} where qualifiers may be represented by annotations with elements.
 *
 * <p>ElementQualifierHierarchy uses a {@link QualifierKindHierarchy} to model the relationships
 * between qualifiers. (By contrast, {@link MostlyNoElementQualifierHierarchy} uses the {@link
 * QualifierKindHierarchy} to implement {@code isSubtype}, {@code leastUpperBound}, and {@code
 * greatestLowerBound} methods for qualifiers without elements.)
 *
 * <p>Subclasses can override {@link #createQualifierKindHierarchy(Collection)} to return a subclass
 * of QualifierKindHierarchy.
 */
@AnnotatedFor("nullness")
public abstract class ElementQualifierHierarchy extends QualifierHierarchy {

  /** {@link org.checkerframework.javacutil.ElementUtils}. */
  private final Elements elements;

  /** {@link QualifierKindHierarchy}. */
  protected final QualifierKindHierarchy qualifierKindHierarchy;

  // The following fields duplicate information in qualifierKindHierarchy, but using
  // AnnotationMirrors instead of QualifierKinds.

  /** A mapping from top QualifierKinds to their corresponding AnnotationMirror. */
  protected final Map<QualifierKind, AnnotationMirror> topsMap;

  /** The set of top annotation mirrors. */
  protected final AnnotationMirrorSet tops;

  /** A mapping from bottom QualifierKinds to their corresponding AnnotationMirror. */
  protected final Map<QualifierKind, AnnotationMirror> bottomsMap;

  /** The set of bottom annotation mirrors. */
  protected final AnnotationMirrorSet bottoms;

  /**
   * A mapping from QualifierKind to AnnotationMirror for all qualifiers whose annotations do not
   * have elements.
   */
  protected final Map<QualifierKind, AnnotationMirror> kindToElementlessQualifier;

  /**
   * Creates a ElementQualifierHierarchy from the given classes.
   *
   * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy
   * @param elements element utils
   * @param atypeFactory the associated type factory
   */
  @SuppressWarnings("this-escape")
  protected ElementQualifierHierarchy(
      Collection<Class<? extends Annotation>> qualifierClasses,
      Elements elements,
      GenericAnnotatedTypeFactory<?, ?, ?, ?> atypeFactory) {
    super(atypeFactory);

    this.elements = elements;
    this.qualifierKindHierarchy = createQualifierKindHierarchy(qualifierClasses);

    this.topsMap = Collections.unmodifiableMap(createTopsMap());
    this.tops = AnnotationMirrorSet.unmodifiableSet(topsMap.values());

    this.bottomsMap = Collections.unmodifiableMap(createBottomsMap());
    this.bottoms = AnnotationMirrorSet.unmodifiableSet(bottomsMap.values());

    this.kindToElementlessQualifier = createElementlessQualifierMap();
  }

  @Override
  public boolean isValid() {
    for (AnnotationMirror top : tops) {
      // This throws an error if poly is a qualifier that has an element.
      getPolymorphicAnnotation(top);
    }
    return true;
  }

  /**
   * Create the {@link QualifierKindHierarchy}. (Subclasses may override to return a subclass of
   * QualifierKindHierarchy.)
   *
   * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy
   * @return the newly created qualifier kind hierarchy
   */
  protected QualifierKindHierarchy createQualifierKindHierarchy(
      @UnderInitialization ElementQualifierHierarchy this,
      Collection<Class<? extends Annotation>> qualifierClasses) {
    return new DefaultQualifierKindHierarchy(qualifierClasses);
  }

  /**
   * Creates a mapping from QualifierKind to AnnotationMirror for all qualifiers whose annotations
   * do not have elements.
   *
   * @return the mapping
   */
  @RequiresNonNull({"this.qualifierKindHierarchy", "this.elements"})
  protected Map<QualifierKind, AnnotationMirror> createElementlessQualifierMap(
      @UnderInitialization ElementQualifierHierarchy this) {
    Map<QualifierKind, AnnotationMirror> quals = new TreeMap<>();
    for (QualifierKind kind : qualifierKindHierarchy.allQualifierKinds()) {
      if (!kind.hasElements()) {
        quals.put(kind, AnnotationBuilder.fromClass(elements, kind.getAnnotationClass()));
      }
    }
    return Collections.unmodifiableMap(quals);
  }

  /**
   * Creates a mapping from QualifierKind to AnnotationMirror, where the QualifierKind is top and
   * the AnnotationMirror is top in their respective hierarchies.
   *
   * <p>This implementation works if the top annotation has no elements, or if it has elements,
   * provides a default, and that default is the top. Otherwise, subclasses must override this.
   *
   * @return a mapping from top QualifierKind to top AnnotationMirror
   */
  @RequiresNonNull({"this.qualifierKindHierarchy", "this.elements"})
  protected Map<QualifierKind, AnnotationMirror> createTopsMap(
      @UnderInitialization ElementQualifierHierarchy this) {
    Map<QualifierKind, AnnotationMirror> topsMap = new TreeMap<>();
    for (QualifierKind kind : qualifierKindHierarchy.getTops()) {
      topsMap.put(kind, AnnotationBuilder.fromClass(elements, kind.getAnnotationClass()));
    }
    return topsMap;
  }

  /**
   * Creates a mapping from QualifierKind to AnnotationMirror, where the QualifierKind is bottom and
   * the AnnotationMirror is bottom in their respective hierarchies.
   *
   * <p>This implementation works if the bottom annotation has no elements, or if it has elements,
   * provides a default, and that default is the bottom. Otherwise, subclasses must override this.
   *
   * @return a mapping from bottom QualifierKind to bottom AnnotationMirror
   */
  @RequiresNonNull({"this.qualifierKindHierarchy", "this.elements"})
  protected Map<QualifierKind, AnnotationMirror> createBottomsMap(
      @UnderInitialization ElementQualifierHierarchy this) {
    Map<QualifierKind, AnnotationMirror> bottomsMap = new TreeMap<>();
    for (QualifierKind kind : qualifierKindHierarchy.getBottoms()) {
      bottomsMap.put(kind, AnnotationBuilder.fromClass(elements, kind.getAnnotationClass()));
    }
    return bottomsMap;
  }

  /**
   * Returns the qualifier kind for the given annotation.
   *
   * @param anno an annotation mirror that is in this hierarchy
   * @return the qualifier kind for the given annotation
   */
  protected QualifierKind getQualifierKind(AnnotationMirror anno) {
    String name = AnnotationUtils.annotationName(anno);
    QualifierKind result = getQualifierKind(name);
    if (result == null) {
      throw new BugInCF("No qualifier kind for " + anno);
    }
    return result;
  }

  /**
   * Returns the qualifier kind for the annotation with the canonical name {@code name}.
   *
   * @param name fully qualified annotation name
   * @return the qualifier kind for the annotation named {@code name}
   */
  protected QualifierKind getQualifierKind(@CanonicalName String name) {
    QualifierKind kind = qualifierKindHierarchy.getQualifierKind(name);
    if (kind == null) {
      throw new BugInCF("QualifierKind %s not in hierarchy", name);
    }
    return kind;
  }

  @Override
  public AnnotationMirrorSet getTopAnnotations() {
    return tops;
  }

  @Override
  public AnnotationMirror getTopAnnotation(AnnotationMirror start) {
    QualifierKind kind = getQualifierKind(start);
    @SuppressWarnings("nullness:assignment") // All tops are a key for topsMap.
    @NonNull AnnotationMirror result = topsMap.get(kind.getTop());
    return result;
  }

  @Override
  public AnnotationMirrorSet getBottomAnnotations() {
    return bottoms;
  }

  @Override
  public @Nullable AnnotationMirror getPolymorphicAnnotation(AnnotationMirror start) {
    QualifierKind polyKind = getQualifierKind(start).getPolymorphic();
    if (polyKind == null) {
      return null;
    }
    AnnotationMirror poly = kindToElementlessQualifier.get(polyKind);
    if (poly == null) {
      throw new TypeSystemError(
          "Poly %s has an element. Override ElementQualifierHierarchy#getPolymorphicAnnotation.",
          polyKind);
    }
    return poly;
  }

  @Override
  public boolean isPolymorphicQualifier(AnnotationMirror qualifier) {
    return getQualifierKind(qualifier).isPoly();
  }

  @Override
  public AnnotationMirror getBottomAnnotation(AnnotationMirror start) {
    QualifierKind kind = getQualifierKind(start);
    @SuppressWarnings("nullness:assignment") // All bottoms are keys for bottomsMap.
    @NonNull AnnotationMirror result = bottomsMap.get(kind.getBottom());
    return result;
  }

  @Override
  public @Nullable AnnotationMirror findAnnotationInSameHierarchy(
      Collection<? extends AnnotationMirror> annos, AnnotationMirror annotationMirror) {
    QualifierKind kind = getQualifierKind(annotationMirror);
    for (AnnotationMirror candidate : annos) {
      QualifierKind candidateKind = getQualifierKind(candidate);
      if (candidateKind.isInSameHierarchyAs(kind)) {
        return candidate;
      }
    }
    return null;
  }

  @Override
  public @Nullable AnnotationMirror findAnnotationInHierarchy(
      Collection<? extends AnnotationMirror> annos, AnnotationMirror top) {
    return findAnnotationInSameHierarchy(annos, top);
  }
}
